// 拟实现的功能分解
// 1. 新创建的域应该能够自动生成admin、teacher、student、vip等角色，并设置角色权限(✅)
// 2. 生成一个对应域的管理员账号和密码，并自动加入该域✅
// 3. 该域的管理员可以生成教师账号和学生账号（批量），批量修改学生密码✅，后期设置学生的使用期限等功能❌
// 4. 学生和老师登陆自动进入对应域✅
// 5. 管理员和老师能访问system域进行题目的拉取，即将该题目放到自己的域中,考虑在system域中设置相应的角色来实现✅
// 6. 超级管理员管理各个域（学校或机构）的到期日期，✅到期后不能使用高级功能（包括但不限于以下功能：总用户数超过100，✅管理班级/小组，批量加入会员用户，复制作业、比赛和训练，AI相关功能，等等❌
// 7. 后期考虑如何复制作业和训练❌
// 8. 后期考虑如何加入AI相关功能❌
//
import {
  _,
  Context,
  requireSudo,
  DiscussionNotFoundError,
  RoleAlreadyExistError,
  DocumentModel,
  Filter,
  moment,
  Handler,
  NumberKeys,
  ObjectId,
  BadRequestError,
  Dictionary,
  NotFoundError,
  OplogModel,
  param,
  Schema,
  PRIV,
  Types,
  UserModel,
  DomainModel,
  DomainDoc,
  SettingModel,
  PERM,
  ForbiddenError,
} from "hydrooj";
import { deleteUserCache } from "hydrooj/src/model/user";
import hash from "hydrooj/src/lib/hash.hydro";
type UIInjectableFields =
  | "ProblemAdd"
  | "Notification"
  | "Nav"
  | "UserDropdown"
  | "DomainManage"
  | "ControlPanel"
  | "SchoolManage";

class ManageHandler extends Handler {
  async prepare({ domainId }) {
    this.checkPerm(PERM.PERM_VIEW_HIDDEN_HOMEWORK);
    this.domain = await DomainModel.get(domainId);
    if (this.user.role != "root" && this.domain._id === "system")
      throw new ForbiddenError();
  }
}
//管理所有会员单位（学校）的到期日期，开启，续期一年等
class ManagememberSchoolHandler extends ManageHandler {
  async get({}) {
    let ddocs: DomainDoc[] = [];
    const role: Record<string, string> = {};
    this.checkPriv(PRIV.PRIV_VIEW_ALL_DOMAIN);
    ddocs = await DomainModel.getMulti().toArray();
    const canManage = {};
    if (this.user.hasPriv(PRIV.PRIV_MANAGE_ALL_DOMAIN)) {
      for (const ddoc of ddocs) {
        canManage[ddoc._id] = true;
        role[ddoc._id] = "root";
      }
    } else {
      for (const ddoc of ddocs) {
        // eslint-disable-next-line no-await-in-loop
        const udoc = await UserModel.getById(ddoc._id, this.user._id);
        canManage[ddoc._id] = udoc.hasPerm(PERM.PERM_EDIT_DOMAIN);
        role[ddoc._id] = udoc.role;
      }
    }
    this.response.template = "managemember_school.html";
    this.response.body = { ddocs, canManage, role };
  }
  @param("id", Types.String)
  async postRenew({}, id: string) {
    const ddoc = await DomainModel.get(id);
    if (!ddoc) throw new NotFoundError(id);
    const curDate = moment(ddoc.expiring).isSameOrAfter(moment())
      ? moment(ddoc.expiring).add(1, "years").toDate()
      : moment().add(1, "years").toDate();
    const update = { $set: { expiring: new Date(curDate) } };

    await Promise.all([
      DomainModel.coll.findOneAndUpdate({ _id: id }, update, {}),
      OplogModel.log(this, "Domain.addOneYearforExpiring", { id, update }),
    ]);
    bus.broadcast("domain/delete-cache", id.toLowerCase());
    this.response.redirect = this.url("managemembers");
  }

  @param("id", Types.String)
  @param("months", Types.Int)
  async postAddmonth({}, id: string, months: number) {
    const ddoc = await DomainModel.get(id);
    if (!ddoc) throw new NotFoundError(id);
    const newExpiring = moment(ddoc.expiring).isAfter(moment())
      ? moment(ddoc.expiring).add(months, "months").toDate()
      : moment().add(months, "months").toDate();
    const update = { $set: { expiring: new Date(newExpiring) } };
    await Promise.all([
      DomainModel.coll.findOneAndUpdate({ _id: id }, update, {}),
      OplogModel.log(this, "Domain.addOneYearforExpiring", { id, update }),
    ]);
    bus.broadcast("domain/delete-cache", id.toLowerCase());
    this.back();
  }
}
class SchoolmanageGroupHandler extends ManageHandler {
  async prepare({ domainId }) {
    if (moment(this.domain.expiring).isBefore(moment())) {
      throw new ForbiddenError("会员学校已经到期。");
    }
  }
  async get({ domainId }) {
    this.response.template = "schoolmanage_group.html";
    this.response.body = {
      domain: this.domain,
      groups: await UserModel.listGroup(domainId),
    };
  }

  @param("name", Types.Name)
  async postDel(domainId: string, name: string) {
    await UserModel.delGroup(domainId, name);
    this.back();
  }

  @param("name", Types.Name)
  @param("uids", Types.NumericArray)
  async postUpdate(domainId: string, name: string, uids: number[]) {
    await UserModel.updateGroup(domainId, name, uids);
    this.back();
  }
}
class SchoolmanageDashboardHandler extends ManageHandler {
  async get() {
    const owner = await UserModel.getById(this.domain._id, this.domain.owner);
    this.response.template = "schoolmanage_dashboard.html";
    this.response.body = {
      domain: this.domain,
      owner,
      expiring: this.domain.expiring,
    };
  }
}
class SchoolmanageUserHandler extends ManageHandler {
  @requireSudo
  @param("format", Types.Range(["default", "raw"]), true)
  async get({ domainId }, format = "default") {
    const rudocs = {};
    const userscnt = await DomainModel.collUser.countDocuments({
      domainId: this.domain._id,
      role: { $nin: ["admin", "root", "default", "guest"] },
    });
    const [dudocs, rolesori] = await Promise.all([
      DomainModel.getMultiUserInDomain(domainId, {
        $and: [
          { role: { $nin: ["admin", "root", "default", "guest"] } },
          { role: { $ne: null } },
        ],
      }).toArray(),
      DomainModel.getRoles(domainId),
    ]);
    //  switch to getListForRender for better performance
    const udict = await UserModel.getList(
      domainId,
      dudocs.map((dudoc) => dudoc.uid)
    );
    const roles = rolesori.filter((role) => {
      return (
        role._id == "student" || role._id == "teacher" || role._id == "vip"
      );
    });
    const users = dudocs.filter(
      (dudoc) => udict[dudoc.uid].priv & PRIV.PRIV_USER_PROFILE
    );
    for (const role of roles) {
      if (role._id == "root") continue;
      rudocs[role._id] = users
        .filter((dudoc) => dudoc.role === role._id)
        .map((i) => udict[i.uid]);
    }
    this.response.template =
      format === "raw"
        ? "schoolmanage_user_raw.html"
        : "schoolmanage_user.html";
    this.response.body = {
      roles,
      rudocs,
      domain: this.domain,
      userscnt,
    };
  }

  @param("uids", Types.NumericArray)
  async post({}, uids: number[]) {
    if (this.user.role != "admin" && this.user.role != "teacher")
      throw new ForbiddenError();
  }

  @requireSudo
  @param("uids", Types.NumericArray)
  @param("role", Types.Role)
  async postSetUsers(domainId: string, uid: number[], role: string) {
    await Promise.all([
      DomainModel.setUserRole(domainId, uid, role),
      OplogModel.log(this, "domain.setRole", { uid, role }),
    ]);
    // 如果是设置teacher角色，需要添加主题库访问权限
    // 如果是设置非teacher角色，需要去掉主题库访问权限
    const newDomainId = "system";
    const newRole = role == "teacher" ? "school_admin" : "guest";
    await Promise.all([
      DomainModel.setUserRole(newDomainId, uid, newRole),
      OplogModel.log(this, "domain.setRole", {
        domainId: "system",
        uid,
        newRole,
      }),
    ]);
    this.back();
  }
  @requireSudo
  @param("uids", Types.NumericArray)
  async postResetpass({ domainId }, uids: number[]) {
    const affected = await UserModel.getMulti({ _id: { $in: uids } })
      .project<{ _id: number; mail: string; uname: string }>({
        mail: 1,
        uname: 1,
      })
      .toArray();
    const password = "abc123";
    for (const udoc of affected) deleteUserCache(udoc);
    const salt = String.random();
    const update = {
      $set: { salt, hash: await hash(password, salt), hashType: "hydro" },
    };
    await Promise.all([
      UserModel.coll.updateMany({ _id: { $in: uids } }, update, {
        upsert: false,
      }),
      OplogModel.log(this, "user.resetpass", { uids }),
    ]);
    deleteUserCache(this.domain._id);
    this.back();
  }
}
class SchoolmanageAdduserHandler extends ManageHandler {
  @requireSudo
  async get() {
    this.response.body.users = [];
    this.response.template = "schoolmanage_adduser.html";
  }
  @param("users", Types.Content)
  @param("draft", Types.Boolean)
  async post(domainId: string, _users: string, draft: boolean) {
    const users = _users.split("\n");
    const udocs: {
      email: string;
      username: string;
      password: string;
      displayName?: string;
      [key: string]: any;
    }[] = [];
    const messages = [];
    const mapping = Object.create(null);
    const groups: Record<string, string[]> = Object.create(null);
    for (const i in users) {
      const u = users[i];
      if (!u.trim()) continue;
      let [email, username, password, displayName, extra] = u
        .split("\t")
        .map((t) => t.trim());
      if (!email || !username || !password) {
        const data = u.split(",").map((t) => t.trim());
        [email, username, password, displayName, extra] = data;
        if (data.length > 5) extra = data.slice(4).join(",");
      }
      Types.Boolean;
      if (email && username && password) {
        if (!Types.Email[1](email))
          messages.push(`Line ${+i + 1}: Invalid email.`);
        else if (!Types.Username[1](username))
          messages.push(`Line ${+i + 1}: Invalid username`);
        else if (!Types.Password[1](password))
          messages.push(`Line ${+i + 1}: Invalid password`);
        else if (
          udocs.find((t) => t.email === email) ||
          (await UserModel.getByEmail("system", email))
        ) {
          messages.push(`Line ${+i + 1}: Email ${email} already exists.`);
        } else if (
          udocs.find((t) => t.username === username) ||
          (await UserModel.getByUname("system", username))
        ) {
          messages.push(`Line ${+i + 1}: Username ${username} already exists.`);
        } else {
          const payload: any = {};
          try {
            const data = JSON.parse(extra);
            if (data.group) {
              groups[data.group] ||= [];
              groups[data.group].push(email);
            }
            Object.assign(payload, data);
          } catch (e) {}
          Object.assign(payload, {
            email,
            username,
            password,
            displayName,
          });
          await this.ctx.serial("user/import/parse", payload);
          udocs.push(payload);
        }
      } else messages.push(`Line ${+i + 1}: Input invalid.`);
    }
    const [dudocs] = await Promise.all([
      DomainModel.getMultiUserInDomain(domainId, {
        $and: [
          { role: { $nin: ["admin", "root", "default", "guest"] } },
          { role: { $ne: null } },
        ],
      }).toArray(),
    ]);
    const limits = moment(this.domain.expiring).isAfter(moment()) ? 5000 : 100;
    const sumofusers = dudocs.length;
    const leftcnt = limits - sumofusers;
    if (leftcnt <= 0)
      messages.push(
        `总共有${sumofusers}个用户了，没有用户名额了，请升级会员单位解决，具体联系威海市青少年科技教育协会。`
      );
    if (leftcnt - udocs.length < 0)
      messages.push(
        `总共剩余${leftcnt}个用户了，要添加${udocs.length}个用户，请升级会员单位解决，具体联系威海市青少年科技教育协会。`
      );
    if (udocs.length > 100)
      messages.push(
        `The number of users has exceeded 100, but the maximum is 100.`
      );
    else messages.push(`${udocs.length} users found.`);

    if (!draft && udocs.length <= 100 && leftcnt >= udocs.length) {
      for (const udoc of udocs) {
        try {
          const uid = await UserModel.create(
            udoc.email,
            udoc.username,
            udoc.password
          );
          mapping[udoc.email] = uid;
          if (udoc.displayName || 1)
            await DomainModel.setUserInDomain(domainId, uid, {
              displayName: udoc.displayName,
            });
          // 设置角色
          const role = "student";
          await Promise.all([
            DomainModel.setUserRole(domainId, uid, role),
            OplogModel.log(this, "domain.setRole", { uid, role }),
          ]);
          await Promise.all([
            DomainModel.setUserRole("system", uid, "guest"),
            OplogModel.log(this, "domain.system.setRole", {
              uid,
              role: "guest",
            }),
          ]);
          //设置学校
          if (udoc.school)
            await UserModel.setById(uid, { school: udoc.school });
          //设置学号
          if (udoc.studentId)
            await UserModel.setById(uid, { studentId: udoc.studentId });
          await this.ctx.serial("user/import/create", uid, udoc);
        } catch (error) {
          messages.push(error.message);
        }
      }
      const existing = await UserModel.listGroup(domainId);
      for (const name in groups) {
        const uids = groups[name].map((i) => mapping[i]).filter((i) => i);
        const current = existing.find((i) => i.name === name)?.uids || [];
        if (uids.length)
          await UserModel.updateGroup(
            domainId,
            name,
            Array.from(new Set([...current, ...uids]))
          );
      }
    }
    this.response.body.users = udocs;
    this.response.body.messages = messages;
  }
}
class SchoolmanageEditHandler extends ManageHandler {
  async get() {
    this.response.template = "schoolmanage_edit.html";
    this.response.body = {
      current: this.domain,
      settings: SettingModel.DOMAIN_SETTINGS,
    };
  }
  async post(args) {
    if (args.operation) return;
    const $set = {};
    const booleanKeys = args.booleanKeys || {};
    delete args.booleanKeys;
    for (const key in booleanKeys) if (!args[key]) $set[key] = false;
    for (const key in args) {
      if (SettingModel.DOMAIN_SETTINGS_BY_KEY[key]) $set[key] = args[key];
    }
    await DomainModel.edit(args.domainId, $set);
    this.response.redirect = this.url("schoolmanage_dashboard");
  }
}
export async function apply(ctx: Context) {
  // 注册一个名为 paste_create 的路由，匹配 '/paste/create'，
  // 使用PasteCreateHandler处理，访问改路由需要PRIV.PRIV_USER_PROFILE权限
  // 提示：路由匹配基于 path-to-regexp
  ctx.Route(
    "schoolmanage",
    "/schoolmanage/dashboard",
    SchoolmanageDashboardHandler,
    PERM.PERM_VIEW_HIDDEN_HOMEWORK
  );
  ctx.Route(
    "schoolmanage_dashboard",
    "/schoolmanage/dashboard",
    SchoolmanageDashboardHandler,
    PERM.PERM_VIEW_HIDDEN_HOMEWORK
  );
  ctx.Route(
    "schoolmanage_edit",
    "/schoolmanage/edit",
    SchoolmanageEditHandler,
    PERM.PERM_VIEW_HIDDEN_HOMEWORK
  );
  ctx.Route(
    "schoolmanage_user",
    "/schoolmanage/user",
    SchoolmanageUserHandler,
    PERM.PERM_VIEW_HIDDEN_HOMEWORK
  );
  ctx.Route(
    "schoolmanage_adduser",
    "/schoolmanage/adduser",
    SchoolmanageAdduserHandler,
    PERM.PERM_VIEW_HIDDEN_HOMEWORK
  );
  ctx.Route(
    "schoolmanage_group",
    "/schoolmanage/group",
    SchoolmanageGroupHandler,
    PERM.PERM_VIEW_HIDDEN_HOMEWORK
  );
  ctx.injectUI(
    "Nav",
    "schoolmanage",
    { prefix: "schoolmanage" },
    PERM.PERM_VIEW_HIDDEN_HOMEWORK
  );
  ctx.Route(
    "managemembers",
    "/managemember/school",
    ManagememberSchoolHandler,
    PERM.PERM_VIEW_HIDDEN_HOMEWORK
  );
  ctx.injectUI(
    "Nav",
    "managemembers",
    { prefix: "managememberschool" },
    PERM.PERM_EDIT_DOMAIN
  );
  // make SchoolMange 菜单
  ctx.injectUI("SchoolManage", "schoolmanage_dashboard", {
    family: "Properties",
    icon: "info",
  });
  ctx.injectUI("SchoolManage", "schoolmanage_edit", {
    family: "Properties",
    icon: "info",
  });
  ctx.injectUI("SchoolManage", "schoolmanage_adduser", {
    family: "Access Control",
    icon: "user",
  });
  ctx.injectUI("SchoolManage", "schoolmanage_user", {
    family: "Access Control",
    icon: "user",
  });
  ctx.injectUI("SchoolManage", "schoolmanage_group", {
    family: "Advance Control",
    icon: "stack",
  });
  ctx.injectUI("SchoolManage", "schoolmanage_advancesetting", {
    family: "Advance Control",
    icon: "stack",
  });
  // ctx.Route('paste_show', '/paste/show/:id', PasteShowHandler); handler 表示路由事件
  // after 表示在主逻辑完成后运行，同时支持 before, before-operation, 参照 【插件编写】 章节 【请求流程】
  // RecordDetail 为需要捕获的路由名
  // #get 表示仅捕获 GET 请求，若无此后缀表示捕获该路由的所有请求
  //*** 修改不是学校管理员和老师不能访问主题库
  ctx.on("handler/after/Home", notAdminredirectDomain);
  //***** 修改创建域后自动生成相应角色，并设置权限
  ctx.on("handler/after/DomainDashboard#post", createDomainRoleandAdmin);

  ctx.i18n.load("zh", {
    schoolmanage_advancesetting: "高级功能设置",
    managemember_school: "管理会员单位",
    "Expiring Domain": "按月延续会员学校",
    "expiring months": "延续月数",
    expiring: "到期时间",
    Expiring: "到期时间",
    "member expiring": "会员学校到期时间",
    renew: "延续（一年）",
    managemembers: "管理会员单位",
    schoolmanage_group: "管理班级/小组",
    "Advance Control": "高级功能(会员学校专用)",
    "Init Discussion Nodes": "初始化域角色和管理员及讨论节点",
    schoolmanage: "学校管理",
    schoolmanage_dashboard: "学校管理面板",
    schoolmanage_edit: "修改学校资料",
    schoolmanage_user: "用户管理",
    schoolmanage_adduser: "添加用户",
    resetpass_selected: "重置所选用户密码",
    "Confirm reset password the selected users?":
      "确认将所选用户的密码进行重置吗？",
    "Their account`s password will be reset  and they will login this site with the new password abc123. Please tell them edit passwod after soon.":
      "所选用户的密码将被重置为 abc123 ，为账号安全，请告知学生及时更改密码。",
    "Selected users have been reset password.":
      "所选用户的密码已经重置为 abc123。",
  });
}
//*** 修改不是学校管理员和老师不能访问主题库和其他的域
async function notAdminredirectDomain(h: Handler) {
  //1. get udoc
  // 查看登录没有
  let isLogin = h.user ? 1 : 0;
  if (isLogin) {
    // let ddocs: DomainDoc[] = [];
    const uid = h.user._id;
    const dudict = await DomainModel.getDictUserByDomainId(uid);
    const dids = Object.keys(dudict);
    let domainname = "";
    for (let i = 0; i < dids.length; i++) {
      if (dids[i] != "system") domainname = dids[i];
    }
    let flag = true;
    //  查看用户在system域和应该所在的域的role
    flag = flag && !(h.user.role == "school_admin");
    flag = flag && !(domainname == h.domain._id);
    flag = flag && !(h.user.role == "root");
    flag = flag && !(h.user.role == "admin");
    // ddocs = await DomainModel.getMulti({ _id: { $in: dids } }).toArray();
    // for (let i = 0; i < ddocs.length; i++) {
    //   ddocs[i]. = ddocs[i].avatar
    //     ? avatar(ddocs[i].avatar, 64)
    //     : "/img/team_avatar.png";
    // }
    const realUrl = "/d/" + domainname + "/";
    //
    //2. 设置 redirect url

    if (flag) h.response.redirect = realUrl;
  }
}
// 创建域时自动生成管理员和对应角色
async function createDomainRoleandAdmin(h: Handler) {
  // handler 系列钩子的一参数为对应的 Handler 实例
  // 不允许查看 24 小时以前的提交记录
  // if (h.rdoc._id.getTimestamp() < new Date(Date.now() - Time.day)) {
  //     h.rdoc.code = '';
  // }
  // 1.获取domainID
  const newDomainId: string = h.domain._id;
  // 设置到期日期
  await Promise.all([
    DomainModel.coll.findOneAndUpdate(
      { _id: newDomainId },
      { $set: { expiring: new Date() } }
    ),
    OplogModel.log(h, "domain.join", {}),
  ]);
  let flag = newDomainId != "system";
  // 判断做没有做过
  const role = "admin";
  const roles = await DomainModel.getRoles(h.domain);
  const rdict: Dictionary<any> = {};
  for (const r of roles) rdict[r._id] = r.perm;
  if (rdict[role]) flag = flag && false;
  if (flag) {
    // 2:生成角色
    const role1 = "admin";
    const perm1: bigint =
      10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831654984417156964076746741n;
    // add admin
    await Promise.all([
      DomainModel.addRole(newDomainId, role1, perm1),
      OplogModel.log(h, "domain.addRole", { role1 }),
    ]);
    // add teachr;
    const role2 = "teacher";
    const perm2: bigint =
      10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831654984417156964076746741n;
    await Promise.all([
      DomainModel.addRole(newDomainId, role2, perm2),
      OplogModel.log(h, "domain.addRole", { role2 }),
    ]);
    // add student;
    const role3 = "student";
    const perm3: bigint =
      10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831653995010913575226802881n;
    // const perm3 = PERM.PERM_DEFAULT;
    await Promise.all([
      DomainModel.addRole(newDomainId, role3, perm3),
      OplogModel.log(h, "domain.addRole", { role3 }),
    ]);
    // add vip;
    const role4 = "vip";
    const perm4: bigint =
      10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831653995010913575226802881n;
    await Promise.all([
      DomainModel.addRole(newDomainId, role4, perm4),
      OplogModel.log(h, "domain.addRole", { role4 }),
    ]);
    // 3 生成一个用户名 域名_admin 密码 admin@789的用户，设置该用户为 system域的school_admin 角色 新域的 admin 角色
    const adminPass = "admin@789";
    const adminUser = newDomainId + "_admin";
    const email = newDomainId + "@qq.com";
    // 添加用户
    let udoc = await UserModel.getByUname(newDomainId, adminUser);
    // udoc ||= await user.getByUname(omainId, uname);
    let uid: number = udoc ? udoc._id : 0;
    if (!udoc) uid = await UserModel.create(email, adminUser, adminPass);
    // 以admin角色加入该域
    if (uid > 0)
      await Promise.all([
        DomainModel.setUserInDomain(newDomainId, uid, {
          join: true,
          role: "admin",
        }),
        OplogModel.log(h, "domain.join", {}),
      ]);
    // 以school_admin角色加入system域
    const sysdomain = "system";
    if (uid > 0)
      await Promise.all([
        DomainModel.setUserInDomain(sysdomain, uid, {
          join: true,
          role: "school_admin",
        }),
        OplogModel.log(h, "domain.join", {}),
      ]);
  }
}
